import random
import asyncio
import random
import string
import sys
import threading

import p2pclient.pb.p2pd_pb2 as p2pd_pb2
from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow, QInputDialog, QMessageBox, QLineEdit, QListWidgetItem
from anyio import SocketStream
from anyio.exceptions import ClosedResourceError
from libp2p import ID
from multiaddr import Multiaddr
from p2pclient import Client
from p2pclient.serialization import read_unsigned_varint

import msg_pb2
from p2pchat_window import Ui_MainWindow
from subproc import JobPopen


class MyMainWindow(QMainWindow, Ui_MainWindow):
    curr_room_stream: SocketStream
    p2p_client: Client
    receive = pyqtSignal(str)
    set_peer_id = pyqtSignal(str)
    room_list_refresh = pyqtSignal()
    join_room = pyqtSignal()

    def __init__(self, loop, parent=None):
        super(MyMainWindow, self).__init__(parent)

        self.setupUi(self)
        self.loop = loop

        self.name = None
        self.p2pd = None
        self.p2p_client = None
        self.room_dict = {}
        self.curr_room = None
        self.curr_room_stream = None

        self.run_node_pushButton.clicked.connect(self.run_node_qt)
        self.connect_peer_pushButton.clicked.connect(self.connect_peer_qt)
        self.room_refresh_pushButton.clicked.connect(self.room_refresh_qt)
        self.room_create_pushButton.clicked.connect(self.room_create_qt)
        self.send_pushButton.clicked.connect(self.send_qt)

        self.room_listWidget.doubleClicked.connect(self.room_list_clicked_qt)

        self.receive.connect(self.do_receive)
        self.set_peer_id.connect(self.do_set_peer_id_qt)
        self.room_list_refresh.connect(self.do_room_list_refresh_qt)
        self.join_room.connect(self.join_room_qt)

    def run_node_qt(self):
        self.name = self.name_lineEdit.text()
        if self.name == "":
            QMessageBox.critical(self, "错误", "请输入名字", QMessageBox.Ok, QMessageBox.Ok)
            return

        def run_node_thread():
            asyncio.ensure_future(self.run_node_thread(), loop=self.loop)

        self.loop.call_soon_threadsafe(run_node_thread)
        pass

    async def run_node_thread(self) -> None:
        if self.p2p_client is not None:
            return
        port1 = 8000 + random.randint(1, 500)
        control_maddr_str = f"/ip4/127.0.0.1/tcp/{port1}"
        port2 = 8500 + random.randint(1, 500)
        listen_maddr_str = f"/ip4/127.0.0.1/tcp/{port2}"
        self.p2pd = JobPopen(f"p2pd -listen {control_maddr_str} -b -pubsub -dht")

        self.p2p_client = Client(
            control_maddr=Multiaddr(control_maddr_str), listen_maddr=Multiaddr(listen_maddr_str)
        )
        name_rsp = await self.p2p_client.pubsub_subscribe("chat_room_name_rsp")
        asyncio.ensure_future(self.chat_room_name_rsp_thread(name_rsp), loop=self.loop)

        name_req = await self.p2p_client.pubsub_subscribe("chat_room_name_req")
        asyncio.ensure_future(self.chat_room_name_req_thread(name_req), loop=self.loop)

        asyncio.ensure_future(self.chat_room_receive_thread(), loop=self.loop)

        p_id, _ = await self.p2p_client.identify()
        self.set_peer_id.emit(str(p_id))

    def do_set_peer_id_qt(self, peer_id):
        self.peer_id_lineEdit.setText(str(peer_id))

    def connect_peer_qt(self):
        peer_id = self.peer_lineEdit.text()
        if len(peer_id) == 0:
            return

        def connect_peer_warp_thread():
            asyncio.ensure_future(self.connect_peer_thread(peer_id), loop=self.loop)

        self.loop.call_soon_threadsafe(connect_peer_warp_thread)

    async def connect_peer_thread(self, peer_adder):
        peer_info = await self.p2p_client.dht_find_peer(ID.from_base58(peer_adder))
        print((peer_info.peer_id, peer_info.addrs))
        await self.p2p_client.connect(peer_info.peer_id, peer_info.addrs)

    async def chat_room_name_rsp_thread(self, chat_room):
        while True:
            try:
                msg_len = await read_unsigned_varint(chat_room)
                data = await chat_room.receive_exactly(msg_len)
                ps_msg = p2pd_pb2.PSMessage()
                ps_msg.ParseFromString(data)

                room_info = msg_pb2.RoomInfo()
                room_info.ParseFromString(ps_msg.data)

                if room_info.room_id not in self.room_dict:
                    self.room_dict[room_info.room_id] = room_info
                self.room_list_refresh.emit()

            except Exception as e:
                print(e)

    async def chat_room_name_req_thread(self, chat_room):
        while True:
            try:
                msg_len = await read_unsigned_varint(chat_room)
                data = await chat_room.receive_exactly(msg_len)
                ps_msg = p2pd_pb2.PSMessage()
                ps_msg.ParseFromString(data)
                if ps_msg.data.decode("utf-8") != "req":
                    continue
                if self.curr_room is None:
                    continue
                data = self.curr_room.SerializeToString()
                await self.p2p_client.pubsub_publish("chat_room_name_rsp", data)
            except Exception as e:
                print(e)

    def do_room_list_refresh_qt(self):
        self.room_listWidget.clear()
        for k in self.room_dict:
            item = QListWidgetItem()
            item.setText(self.room_dict[k].room_name)
            item.setData(Qt.UserRole, k)
            self.room_listWidget.addItem(item)

    def room_refresh_qt(self):
        def room_refresh_warp_thread():
            asyncio.ensure_future(self.room_refresh_thread(), loop=self.loop)

        self.loop.call_soon_threadsafe(room_refresh_warp_thread)

    async def room_refresh_thread(self):
        # peers = await self.p2p_client.list_peers()
        # print(peers)
        await self.p2p_client.pubsub_publish("chat_room_name_req", "req".encode("utf-8"))

    def room_create_qt(self):
        text, ok_pressed = QInputDialog.getText(self, "", "聊天室名:", QLineEdit.Normal, "")
        if not ok_pressed:
            return

        def room_create_warp_thread():
            asyncio.ensure_future(self.room_create_thread(text), loop=self.loop)

        self.loop.call_soon_threadsafe(room_create_warp_thread)

    async def room_create_thread(self, room_name):
        room_info = msg_pb2.RoomInfo()
        room_info.room_id = ''.join(random.sample(string.ascii_letters + string.digits, 16))
        room_info.room_name = room_name
        data = room_info.SerializeToString()
        await self.p2p_client.pubsub_publish("chat_room_name_rsp", data)
        await self.join_room_thread(room_info)

    async def join_room_thread(self, room_info):
        if self.curr_room_stream is not None:
            last_s = self.curr_room_stream
            self.curr_room_stream = None
            await last_s.close()
        self.curr_room = room_info
        self.curr_room_stream = await self.p2p_client.pubsub_subscribe("chat_room_" + self.curr_room.room_id)
        self.join_room.emit()

    def join_room_qt(self):
        if self.curr_room is None:
            return
        self.room_name_label.setText("当前房间:" + self.curr_room.room_name)
        self.receive_textEdit.clear()
    async def chat_room_receive_thread(self):
        while True:
            stream = self.curr_room_stream
            if stream is None:
                await asyncio.sleep(1)
                continue
            try:
                msg_len = await read_unsigned_varint(stream)
                if msg_len is None:
                    continue
                data = await stream.receive_exactly(msg_len)
                ps_msg = p2pd_pb2.PSMessage()
                ps_msg.ParseFromString(data)

                msg = msg_pb2.ChatMessage()
                msg.ParseFromString(ps_msg.data)
                if self.curr_room is not None and msg.room_id == self.curr_room.room_id:
                    self.receive.emit(f"{msg.send_name}:{msg.msg}")

            except ClosedResourceError:
                pass
            except Exception as e:
                print(e)

    def room_list_clicked_qt(self, index: QModelIndex):
        k = index.data(Qt.UserRole)

        def room_list_clicked_warp_thread():
            asyncio.ensure_future(self.join_room_thread(self.room_dict[k]), loop=self.loop)

        self.loop.call_soon_threadsafe(room_list_clicked_warp_thread)

    def send_qt(self):
        text = self.send_lineEdit.text()

        def send_warp_thread():
            asyncio.ensure_future(self.send_thread(self.name, text), loop=self.loop)
            pass

        self.loop.call_soon_threadsafe(send_warp_thread)
        self.send_lineEdit.setText("")
        pass

    async def send_thread(self, name, text):
        if self.curr_room is None:
            return
        msg = msg_pb2.ChatMessage()
        msg.room_id = self.curr_room.room_id
        msg.send_name = name
        msg.msg = text
        data = msg.SerializeToString()
        await self.p2p_client.pubsub_publish("chat_room_" + self.curr_room.room_id, data)
        # self.receive.emit(f"{name}:{text}\n")
        pass

    def do_receive(self, text):
        self.receive_textEdit.append(text)


def run_loop_p2p(loop):
    loop.run_forever()


def main():
    app = QApplication(sys.argv)
    loop = asyncio.get_event_loop()
    my_win = MyMainWindow(loop)
    my_win.show()
    t = threading.Thread(target=run_loop_p2p, args=(loop,))
    t.setDaemon(True)
    t.start()
    q = app.exec_()
    sys.exit(q)


if __name__ == "__main__":
    main()
